﻿using System.Linq.Expressions;
using System.Security.Authentication;
using System.Security.Claims;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using Stickers.Common.Authorization;
using Stickers.Common.DataAccess;
using Stickers.WebApi.Models;

namespace Stickers.WebApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class StickersController(ISimplifiedDataAccessor dac, IConfiguration configuration) : ControllerBase
{
    [HttpGet("{id}")]
    [ProtectedResource("stickers-api", "stickers.read")]
    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Sticker))]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetByIdAsync(int id)
    {
        var sticker = await dac.GetByIdAsync<Sticker>(CurrentTenantName, id);
        if (sticker is null) return NotFound($"Sticker with id {id} was not found.");

        return Ok(sticker);
    }

    [HttpPost]
    [ProtectedResource("stickers-api", "stickers.update")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> CreateAsync(Sticker sticker)
    {
        var title = sticker.Title;
        var exists =
            await dac.ExistsAsync<Sticker>(CurrentTenantName, s => s.Title == title && s.UserId == CurrentUserName);
        if (exists) return Conflict($"""Sticker "{sticker.Title}" already exists.""");

        sticker.UserId = CurrentUserName;
        var id = await dac.AddAsync(CurrentTenantName, sticker);
        return CreatedAtAction(nameof(GetByIdAsync), new { id }, sticker);
    }

    [HttpDelete("{id}")]
    [ProtectedResource("stickers-api", "stickers.delete")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> DeleteByIdAsync(int id)
    {
        var result = await dac.RemoveByIdAsync<Sticker>(CurrentTenantName, id);
        if (result == 0) return NotFound($"Sticker with id {id} was not found.");

        return NoContent();
    }

    [HttpGet]
    [ProtectedResource("stickers-api", "stickers.read")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<IActionResult> GetStickersAsync([FromQuery(Name = "sort")] string? sortField = null,
        [FromQuery(Name = "asc")] bool ascending = true, [FromQuery(Name = "size")] int pageSize = 20,
        [FromQuery(Name = "page")] int pageNumber = 1)
    {
        Expression<Func<Sticker, object>> sortExpression = s => s.Id;
        Expression<Func<Sticker, bool>> filterExpression = s => s.UserId == CurrentUserName;
        if (sortField is not null) sortExpression = ConvertToExpression<Sticker, object>(sortField);

        return Ok(await dac.GetPaginatedEntitiesAsync(CurrentTenantName, sortExpression, ascending, pageSize,
            pageNumber, filterExpression));
    }
    
    [HttpPatch("{id}")]
    [ProtectedResource("stickers-api", "stickers.update")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> UpdateStickerAsync(int id, [FromBody] JsonPatchDocument<Sticker>? patchDocument)
    {
        if (patchDocument is null) return BadRequest();
        var sticker = await dac.GetByIdAsync<Sticker>(CurrentTenantName, id);
        if (sticker is null) return NotFound();
        sticker.ModifiedOn = DateTime.UtcNow;
        patchDocument.ApplyTo(sticker, ModelState);
        if (!ModelState.IsValid) return BadRequest(ModelState);
        var affected = await dac.UpdateAsync(CurrentTenantName, id, sticker);
        return Ok(new
        {
            affected, sticker
        });
    }

    private static Expression<Func<TEntity, TProperty>> ConvertToExpression<TEntity, TProperty>(string propertyName)
    {
        if (string.IsNullOrWhiteSpace(propertyName))
            throw new ArgumentNullException($"{nameof(propertyName)} cannot be null or empty.");

        var propertyInfo = typeof(TEntity).GetProperty(propertyName);
        if (propertyInfo is null) throw new ArgumentNullException($"Property {propertyName} is not defined.");

        var parameterExpression = Expression.Parameter(typeof(TEntity), "p");
        var memberExpression = Expression.Property(parameterExpression, propertyInfo);
        if (propertyInfo.PropertyType.IsValueType)
            return Expression.Lambda<Func<TEntity, TProperty>>(Expression.Convert(memberExpression, typeof(object)),
                parameterExpression);

        return Expression.Lambda<Func<TEntity, TProperty>>(memberExpression, parameterExpression);
    }
    
    private string CurrentTenantName
    {
        get
        {
            if (User.Identity is ClaimsIdentity { IsAuthenticated: true } identity)
                return identity.Claims.FirstOrDefault(c => c.Type == "azp")?.Value ??
                       throw new AuthenticationException(
                           "Get current tenant name failed: Claim \"azp\" doesn't exist on the user identity.");

            throw new AuthenticationException("Can't get the current tenant name: User is not authenticated.");
        }
    }

    private string CurrentUserName
    {
        get
        {
            if (User.Identity is ClaimsIdentity { IsAuthenticated: true } identity)
                return identity.Claims.FirstOrDefault(c =>
                           c.Type == (configuration["keycloak:nameClaimType"] ?? "preferred_username"))?.Value ??
                       throw new AuthenticationException(
                           "Get current user name failed: Claim \"preferred_username\" doesn't exist on the user identity.");

            throw new AuthenticationException("Can't get the current user name: User is not authenticated.");
        }
    }
}